#include <cstdlib>
#include <iostream>
#include <mpi.h>
#include <time.h>
#include <fstream>
#include <math.h>
#include <Windows.h>
#include "SimpleMatrix.h"

#define ROWS		4
#define COLUMNS		4
#define TICK_COUNT	3

using namespace std;

int processes; //number of processes
int myrank; //process rank
int *elements; //vector containg the number of rows received from the master by the i-th process
int maxElem; //max elements given to each process

SimpleMatrix<unsigned char> *currentUniverse = NULL;
SimpleMatrix<unsigned char> *nextUniverse = NULL;
SimpleMatrix<unsigned char> *globalUniverse = NULL;

enum spawnConfig { DEBUG, RANDOM, FROM_FILE }; //options for the initial seed

//fill the start matrix according to the specified option
template <typename A>
void spawnSeed(SimpleMatrix<A> *universe, spawnConfig config)
{

	switch(config)
	{
		case RANDOM:
		{
			srand (time(0));
			for (int i = 0; i < universe -> height(); i++)
			{
				for (int j = 0; j < universe -> width(); j++)
				{
					/*create a border of fake dead cells around the universe. This is used to simulate 
					the behaviour of cells at the border.
					*/
					if (i == 0 || j == 0 || i == universe -> height() - 1 || j == universe -> width() - 1)
						universe -> set(i, j, (A)0);
					else
					{
						int choice = rand() % 100;
						if (choice > 15)
							universe -> set(i, j, (A)1);
						else
							universe -> set(i, j, (A)0);
					}

					//cout << "Element: [" << i << "," << j << "] = " <<  (int)universe -> get(i, j) << " ";
				}
			}
			break;
		}

		case DEBUG:
		{
			for (int i = 0; i < universe -> height(); i++)
				for (int j = 0; j < universe -> width(); j++)
					universe -> set(i, j, (A)i);
			break;
		}

		case FROM_FILE:
		{
			ifstream matrixData("C:\\Users\\Francesco\\Documents\\Visual Studio 2010\\Projects\\hpcLife\\hpc\\mpiLife\\Release\\matrix.dat");

			if (!matrixData)
				cerr << "Cannot open file" << endl;

			for (int i = 1; i < universe -> height() - 1; i++)
			{
				for (int j = 1; j < universe -> width() - 1; j++)
				{
					int x;
					matrixData >> x;
					cout << x << " ";
					universe -> set(i, j, (unsigned char)x);

				}
			}

			break;
		}

		default:
		{
			cout << "ERROR: Invalid config option in spawnSeed" << endl;
		}
	}
}

void sendPartialMartrices()
{
	vector<int>sendcounts(processes);
	vector<int>displs(processes);

	displs[0] = 0;
	sendcounts[0] = ((ROWS + 2) / processes + 1) * (COLUMNS + 2);

	for (int i = 1; i < processes - 1; i++)
	{
		displs[i] = displs[i - 1] + sendcounts[i - 1] - 2 * (COLUMNS + 2);
		sendcounts[i] = ((ROWS + 2) / processes + 2) * (COLUMNS + 2);
	}

	displs[processes - 1] = displs[processes - 2] + sendcounts[processes - 2] - 2 * (COLUMNS + 2);
	sendcounts[processes - 1] =  max (0, ((ROWS + 2) * (COLUMNS + 2) - displs[processes - 1]));

	/*if (myrank == 0)
	{
		cout << "DISPLS: " << endl;
		for (int i = 0; i < processes; i++)
			cout << displs[i] << " ";
		cout << "\n";
		
		cout << "SENDCOUNTS: " << endl;
		for (int i = 0; i < processes; i++)
			cout << sendcounts[i] << " ";
		cout << "\n";

	}*/

	MPI_Barrier(MPI_COMM_WORLD);

	vector<unsigned char>sendbuf;
	vector<unsigned char>recvbuf(sendcounts[myrank]);

	if (myrank == 0)
	{
		sendbuf = globalUniverse -> buffer();
		/*cout << "SENDBUF:" << endl;
		for (int i = 0; i < globalUniverse -> height() * globalUniverse -> width() - 1; i++)
			cout << (int)sendbuf[i] << " ";
		cout << "\n";*/
	}

	MPI_Scatterv(&sendbuf[0], &sendcounts[0], &displs[0], MPI_UNSIGNED_CHAR, &recvbuf[0], sendcounts[myrank], MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD);

	/*for (int i = 0; i < sendcounts[myrank]; i++)
		cout << "[" << myrank << "," << (int)recvbuf[i] << "]";
	cout << "\n";*/

	delete currentUniverse;
	currentUniverse = new SimpleMatrix<unsigned char>(&recvbuf[0], sendcounts[myrank], (COLUMNS + 2));
	nextUniverse = new SimpleMatrix<unsigned char>(currentUniverse -> height(), currentUniverse -> width(), 0);
	//cout << "\n\nProcess "<< myrank << " Partial Matrix: " << endl;
	//currentUniverse -> printIndices();
	sendcounts.clear();
	displs.clear();
	recvbuf.clear();
	if (myrank == 0)
	{	
		//delete globalUniverse;
		sendbuf.clear();
	}
}

void gatherPartialMatrices()
{
	vector<int>recvcounts(processes);
	vector<int>displs(processes);
	vector<unsigned char>sendbuf = currentUniverse -> buffer();
	vector<unsigned char>recvbuf;

	displs[0] = 0;
	recvcounts[0] = (ROWS + 2) / processes * (COLUMNS + 2);

	for (int i = 1; i < processes - 1; i++)
	{
		displs[i] = displs[i - 1] + recvcounts[i - 1];
		recvcounts[i] = (ROWS + 2) / processes * (COLUMNS + 2);
	}

	displs[processes - 1] = displs[processes - 2] + recvcounts[processes - 2];
	recvcounts[processes - 1] = (ROWS + 2) * (COLUMNS + 2) - displs[processes - 1];

	/*if (myrank == 0)
	{
		cout << "DISPLS: " << myrank << endl;
		for (int i = 0; i < processes; i++)
			cout << displs[i] << " ";
		cout << endl;
		
		cout << "RECVCOUNTS: " << myrank << endl;
		for (int i = 0; i < processes; i++)
			cout << recvcounts[i] << " ";
		cout << endl;
	}*/

	if (myrank == 0)
		recvbuf.resize((ROWS + 2) * (COLUMNS + 2));

	MPI_Gatherv(&sendbuf[0] + (myrank == 0 ? 0 : COLUMNS + 2), recvcounts[myrank], MPI_UNSIGNED_CHAR, &recvbuf[0], &recvcounts[0], &displs[0], MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD);

	/*if (myrank == 0)
	{
		for (int i = 0; i < (ROWS + 2) * (COLUMNS + 2); i++)
			cout << (int)recvbuf[i] << " ";
		cout << endl;
	}*/

	if (myrank == 0)
	{
		delete globalUniverse;
		globalUniverse = new SimpleMatrix<unsigned char>(&recvbuf[0], (ROWS + 2) * (COLUMNS + 2), (COLUMNS + 2));
		vector<char> symbols(2);
		vector<unsigned char> elements(2);
		symbols[0] = 'X';
		symbols[1] = 'O';
		elements[0] = 0;
		elements[1] = 1;
		cout << "\n\nUniverse Matrix: \n" << endl;
		globalUniverse -> print(symbols, elements);
		cout << "\n\n *** End Universe Matrix *** \n" << endl;
	}

	recvcounts.clear();
	displs.clear();
	sendbuf.clear();

	if (myrank == 0)
		recvbuf.clear();

}

void tick()
{
	for (int i = 0; i < currentUniverse -> height(); i++)
		for (int j = 0; j < currentUniverse -> width(); j++)
			nextUniverse -> set(i, j, currentUniverse -> get(i,j));

	vector<char> symbols(2);
	vector<unsigned char> elements(2);
	symbols[0] = 'X';
	symbols[1] = 'O';
	elements[0] = 0;
	elements[1] = 1;
	/*cout << "Process " << myrank << " Current Universe: \n" << endl;
	currentUniverse -> print(symbols, elements);*/

	for (int i = 1; i < currentUniverse -> height() - 1; i++)
	{
		for (int j = 1; j < currentUniverse -> width() - 1; j++)
		{
			int liveNeighbors =	currentUniverse -> get(i - 1, j - 1) +
								currentUniverse	-> get(i - 1, j) +
								currentUniverse -> get(i - 1, j + 1) +
								currentUniverse -> get(i, j - 1) +
								currentUniverse -> get(i, j + 1) +
								currentUniverse -> get(i + 1, j - 1) +
								currentUniverse -> get(i + 1, j) +
								currentUniverse -> get(i + 1, j + 1);
			
			if (currentUniverse -> get(i,j) == 1)
			{
				if (liveNeighbors < 2 || liveNeighbors > 3)
					nextUniverse -> set(i, j, 0);
			}
			else
				if (liveNeighbors == 3)
					nextUniverse -> set(i, j, 1);
		}
	}

	for (int i = 0; i < nextUniverse -> height(); i++)
		for (int j = 0; j < nextUniverse -> width(); j++)
			currentUniverse -> set(i, j, nextUniverse -> get(i,j));

	/*cout << "\n\nProcess " << myrank << " Next Universe: \n" << endl;
	nextUniverse -> print(symbols, elements);*/

}

void updatePartialMatrices()
{
	if (myrank == 0)
	{
		MPI_Status status;
		vector<unsigned char> sendbuf = currentUniverse -> getRow(currentUniverse -> height() - 2);
		vector<unsigned char> recvbuf(currentUniverse -> width());
		MPI_Send(&sendbuf[0], sendbuf.size(), MPI_UNSIGNED_CHAR, 1, 0, MPI_COMM_WORLD);
		MPI_Recv(&recvbuf[0], recvbuf.size(), MPI_UNSIGNED_CHAR, 1, 1, MPI_COMM_WORLD, &status);
		currentUniverse -> setRow(recvbuf, currentUniverse -> height() - 1);
		sendbuf.clear();
		recvbuf.clear();
	}
	else if (myrank == processes - 1)
	{
		if (myrank % 2 == 0)
		{
			MPI_Status status;
			vector<unsigned char> sendbuf = currentUniverse -> getRow(1);
			vector<unsigned char> recvbuf(currentUniverse -> width());
			MPI_Send(&sendbuf[0], sendbuf.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank, MPI_COMM_WORLD);
			MPI_Recv(&recvbuf[0], recvbuf.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank - 1, MPI_COMM_WORLD, &status);
			currentUniverse -> setRow(recvbuf, 0);
			sendbuf.clear();
			recvbuf.clear();
		}
		else
		{
			MPI_Status status;
			vector<unsigned char> sendbuf = currentUniverse -> getRow(1);
			vector<unsigned char> recvbuf(currentUniverse -> width());
			MPI_Recv(&recvbuf[0], recvbuf.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank - 1, MPI_COMM_WORLD, &status);
			MPI_Send(&sendbuf[0], sendbuf.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank, MPI_COMM_WORLD);
			currentUniverse -> setRow(recvbuf, 0);
			sendbuf.clear();
			recvbuf.clear();
		}
	}
	else
	{
		if (myrank % 2 == 0)
		{
			MPI_Status status;
			vector<unsigned char> sendbuf1 = currentUniverse -> getRow(1);
			vector<unsigned char> sendbuf2 = currentUniverse -> getRow(currentUniverse -> height() - 2);
			vector<unsigned char> recvbuf1(currentUniverse -> width());
			vector<unsigned char> recvbuf2(currentUniverse -> width());
			MPI_Send(&sendbuf1[0], sendbuf1.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank, MPI_COMM_WORLD);
			MPI_Send(&sendbuf2[0], sendbuf2.size(), MPI_UNSIGNED_CHAR, myrank + 1, myrank, MPI_COMM_WORLD);
			MPI_Recv(&recvbuf2[0], recvbuf1.size(), MPI_UNSIGNED_CHAR, myrank + 1, myrank + 1, MPI_COMM_WORLD, &status);
			MPI_Recv(&recvbuf1[0], recvbuf2.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank - 1, MPI_COMM_WORLD, &status);
			currentUniverse -> setRow(recvbuf1, 0);
			currentUniverse -> setRow(recvbuf2, currentUniverse -> height() - 1);
			sendbuf1.clear();
			sendbuf2.clear();
			recvbuf1.clear();
			recvbuf2.clear();

		}
		else
		{
			MPI_Status status;
			vector<unsigned char> sendbuf1 = currentUniverse -> getRow(1);
			vector<unsigned char> sendbuf2 = currentUniverse -> getRow(currentUniverse -> height() - 2);
			vector<unsigned char> recvbuf1(currentUniverse -> width());
			vector<unsigned char> recvbuf2(currentUniverse -> width());
			MPI_Recv(&recvbuf2[0], recvbuf1.size(), MPI_UNSIGNED_CHAR, myrank + 1, myrank + 1, MPI_COMM_WORLD, &status);
			MPI_Recv(&recvbuf1[0], recvbuf2.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank - 1, MPI_COMM_WORLD, &status);
			MPI_Send(&sendbuf1[0], sendbuf1.size(), MPI_UNSIGNED_CHAR, myrank - 1, myrank, MPI_COMM_WORLD);
			MPI_Send(&sendbuf2[0], sendbuf2.size(), MPI_UNSIGNED_CHAR, myrank + 1, myrank, MPI_COMM_WORLD);
			currentUniverse -> setRow(recvbuf1, 0);
			currentUniverse -> setRow(recvbuf2, currentUniverse -> height() - 1);
			sendbuf1.clear();
			sendbuf2.clear();
			recvbuf1.clear();
			recvbuf2.clear();
		}
	}

	vector<char>symbols(2);
	vector<unsigned char>elements(2);
	symbols[0] = 'X';
	symbols[1] = 'O';
	elements[0] = 0;
	elements[1] = 1;
	cout << "\n\nProcess "<< myrank << " Partial Matrix: " << endl;
	currentUniverse -> print(symbols, elements);
}

int main(int argc, char* argv[])
{
	MPI_Init(&argc, &argv);
	MPI_Comm_size(MPI_COMM_WORLD, &processes);
	MPI_Comm_rank(MPI_COMM_WORLD, &myrank);

	if (myrank == 0)
	{
		globalUniverse = new SimpleMatrix<unsigned char>(ROWS + 2, COLUMNS + 2, 0);
		spawnSeed(globalUniverse, FROM_FILE);
		vector<char>symbols(2);
		vector<unsigned char>elements(2);
		symbols[0] = 'X';
		symbols[1] = 'O';
		elements[0] = 0;
		elements[1] = 1;
		globalUniverse -> print(symbols, elements);

	}

	/*sendPartialMartrices();

	for (int i = 0; i < TICK_COUNT; i++)
	{
		tick();
		updatePartialMatrices();
		Sleep(5000);
	}

	gatherPartialMatrices();*/


	MPI_Barrier(MPI_COMM_WORLD);
	MPI_Finalize();
	return 0;
}